import os
import json
import shutil
import threading
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from datetime import datetime, time
import schedule
import time as tm
from zipfile import ZipFile, ZIP_DEFLATED

class BackupApp:
    def __init__(self, root):
        self.root = root
        self.root.title("自动备份工具 - 大文件夹专用")
        self.root.geometry("800x600")
        
        # 配置文件名
        self.config_file = "backup_config.json"
        
        # 加载默认配置
        self.config = {
            "source_dir": "",
            "backup_dir": "",
            "backup_time": "00:00",
            "max_backups": 7,
            "compression_level": 6,
            "exclude_extensions": [".tmp", ".log"],
            "exclude_folders": ["temp", "cache"]
        }
        
        # 加载保存的配置
        self.load_config()
        
        # 创建UI
        self.create_widgets()
        
        # 启动调度线程
        self.schedule_thread = threading.Thread(target=self.run_scheduler, daemon=True)
        self.schedule_thread.start()
    
    def create_widgets(self):
        # 主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # 源目录选择
        source_frame = ttk.LabelFrame(main_frame, text="源目录设置", padding="10")
        source_frame.pack(fill=tk.X, pady=5)
        
        ttk.Label(source_frame, text="要备份的目录:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.source_entry = ttk.Entry(source_frame, width=70)
        self.source_entry.grid(row=0, column=1, padx=5, pady=5, sticky="we")
        self.source_entry.insert(0, self.config["source_dir"])
        ttk.Button(source_frame, text="浏览...", command=self.select_source).grid(row=0, column=2, padx=5, pady=5)
        
        # 备份目录选择
        backup_frame = ttk.LabelFrame(main_frame, text="备份设置", padding="10")
        backup_frame.pack(fill=tk.X, pady=5)
        
        ttk.Label(backup_frame, text="备份保存位置:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.backup_entry = ttk.Entry(backup_frame, width=70)
        self.backup_entry.grid(row=0, column=1, padx=5, pady=5, sticky="we")
        self.backup_entry.insert(0, self.config["backup_dir"])
        ttk.Button(backup_frame, text="浏览...", command=self.select_backup).grid(row=0, column=2, padx=5, pady=5)
        
        # 备份时间和其他选项
        options_frame = ttk.Frame(backup_frame)
        options_frame.grid(row=1, column=0, columnspan=3, sticky="we")
        
        ttk.Label(options_frame, text="备份时间 (HH:MM):").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.time_entry = ttk.Entry(options_frame, width=10)
        self.time_entry.grid(row=0, column=1, padx=5, pady=5, sticky="w")
        self.time_entry.insert(0, self.config["backup_time"])
        
        ttk.Label(options_frame, text="最大备份数量:").grid(row=0, column=2, padx=5, pady=5, sticky="w")
        self.max_backups_spin = ttk.Spinbox(options_frame, from_=1, to=365, width=5)
        self.max_backups_spin.grid(row=0, column=3, padx=5, pady=5, sticky="w")
        self.max_backups_spin.set(self.config["max_backups"])
        
        ttk.Label(options_frame, text="压缩级别 (0-9):").grid(row=0, column=4, padx=5, pady=5, sticky="w")
        self.compression_spin = ttk.Spinbox(options_frame, from_=0, to=9, width=5)
        self.compression_spin.grid(row=0, column=5, padx=5, pady=5, sticky="w")
        self.compression_spin.set(self.config["compression_level"])
        
        # 排除设置
        exclude_frame = ttk.LabelFrame(main_frame, text="排除设置", padding="10")
        exclude_frame.pack(fill=tk.X, pady=5)
        
        ttk.Label(exclude_frame, text="排除文件扩展名:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.exclude_ext_entry = ttk.Entry(exclude_frame, width=30)
        self.exclude_ext_entry.grid(row=0, column=1, padx=5, pady=5, sticky="w")
        self.exclude_ext_entry.insert(0, ",".join(self.config["exclude_extensions"]))
        
        ttk.Label(exclude_frame, text="排除文件夹名:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
        self.exclude_folder_entry = ttk.Entry(exclude_frame, width=30)
        self.exclude_folder_entry.grid(row=1, column=1, padx=5, pady=5, sticky="w")
        self.exclude_folder_entry.insert(0, ",".join(self.config["exclude_folders"]))
        
        # 按钮区域
        button_frame = ttk.Frame(main_frame)
        button_frame.pack(fill=tk.X, pady=10)
        
        ttk.Button(button_frame, text="保存配置", command=self.save_config).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="立即备份", command=self.perform_backup_now).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="打开备份目录", command=self.open_backup_dir).pack(side=tk.LEFT, padx=5)
        
        # 日志区域
        log_frame = ttk.LabelFrame(main_frame, text="操作日志", padding="10")
        log_frame.pack(fill=tk.BOTH, expand=True)
        
        self.log_text = tk.Text(log_frame, height=15, wrap=tk.WORD)
        self.log_text.pack(fill=tk.BOTH, expand=True)
        
        scrollbar = ttk.Scrollbar(log_frame, command=self.log_text.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.log_text.config(yscrollcommand=scrollbar.set)
        
        # 初始日志
        self.log("备份工具已启动")
        if self.config["source_dir"] and self.config["backup_dir"]:
            self.log(f"已加载配置: 源目录={self.config['source_dir']}")
            self.log(f"备份目录={self.config['backup_dir']}, 时间={self.config['backup_time']}")
            self.schedule_backup()
    
    def select_source(self):
        directory = filedialog.askdirectory()
        if directory:
            self.source_entry.delete(0, tk.END)
            self.source_entry.insert(0, directory)
    
    def select_backup(self):
        directory = filedialog.askdirectory()
        if directory:
            self.backup_entry.delete(0, tk.END)
            self.backup_entry.insert(0, directory)
    
    def open_backup_dir(self):
        backup_dir = self.backup_entry.get()
        if backup_dir and os.path.exists(backup_dir):
            os.startfile(backup_dir)
        else:
            messagebox.showwarning("警告", "备份目录不存在或未设置")
    
    def load_config(self):
        try:
            if os.path.exists(self.config_file):
                with open(self.config_file, "r") as f:
                    loaded_config = json.load(f)
                    # 只更新存在的键，保持默认值不变
                    for key in loaded_config:
                        if key in self.config:
                            self.config[key] = loaded_config[key]
        except Exception as e:
            messagebox.showerror("错误", f"加载配置失败: {str(e)}")
    
    def save_config(self):
        try:
            # 更新配置
            self.config["source_dir"] = self.source_entry.get()
            self.config["backup_dir"] = self.backup_entry.get()
            self.config["backup_time"] = self.time_entry.get()
            self.config["max_backups"] = int(self.max_backups_spin.get())
            self.config["compression_level"] = int(self.compression_spin.get())
            
            # 处理排除设置
            exclude_ext = self.exclude_ext_entry.get().strip()
            self.config["exclude_extensions"] = [ext.strip() for ext in exclude_ext.split(",") if ext.strip()]
            
            exclude_folders = self.exclude_folder_entry.get().strip()
            self.config["exclude_folders"] = [folder.strip() for folder in exclude_folders.split(",") if folder.strip()]
            
            # 验证配置
            if not self.config["source_dir"] or not self.config["backup_dir"]:
                messagebox.showerror("错误", "必须指定源目录和备份目录")
                return
            
            try:
                datetime.strptime(self.config["backup_time"], "%H:%M")
            except ValueError:
                messagebox.showerror("错误", "时间格式不正确，请使用 HH:MM 格式")
                return
            
            # 保存到文件
            with open(self.config_file, "w") as f:
                json.dump(self.config, f, indent=4)
            
            # 重新安排备份任务
            self.schedule_backup()
            
            messagebox.showinfo("成功", "配置已保存")
            self.log("配置已保存并应用")
        except Exception as e:
            messagebox.showerror("错误", f"保存配置失败: {str(e)}")
    
    def schedule_backup(self):
        # 清除所有现有任务
        schedule.clear()
        
        # 添加新任务
        schedule.every().day.at(self.config["backup_time"]).do(self.perform_backup)
        
        self.log(f"已安排每日备份任务，时间: {self.config['backup_time']}")
    
    def perform_backup(self):
        self.log("开始自动备份...")
        try:
            self.perform_backup_now()
        except Exception as e:
            self.log(f"备份失败: {str(e)}")
    
    def perform_backup_now(self):
        try:
            source_dir = self.source_entry.get()
            backup_dir = self.backup_entry.get()
            
            if not source_dir or not backup_dir:
                messagebox.showerror("错误", "必须指定源目录和备份目录")
                return
            
            if not os.path.exists(source_dir):
                messagebox.showerror("错误", "源目录不存在")
                return
            
            if not os.path.exists(backup_dir):
                os.makedirs(backup_dir)
            
            # 创建带时间戳的备份文件名
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            backup_name = f"backup_{timestamp}.zip"
            backup_path = os.path.join(backup_dir, backup_name)
            
            self.log(f"正在备份 {source_dir} 到 {backup_path}...")
            
            # 使用进度对话框
            progress = tk.Toplevel(self.root)
            progress.title("备份进度")
            progress.geometry("400x150")
            progress.resizable(False, False)
            
            tk.Label(progress, text="正在备份文件，请稍候...", font=('Arial', 12)).pack(pady=10)
            
            progress_var = tk.DoubleVar()
            progress_bar = ttk.Progressbar(progress, variable=progress_var, maximum=100, mode="determinate")
            progress_bar.pack(fill=tk.X, padx=20, pady=5)
            
            status_label = tk.Label(progress, text="准备中...")
            status_label.pack(pady=5)
            
            cancel_button = ttk.Button(progress, text="取消", command=lambda: setattr(self, 'cancel_backup', True))
            cancel_button.pack(pady=5)
            
            self.cancel_backup = False
            self.root.update()
            
            # 执行备份
            compression_level = int(self.compression_spin.get())
            total_files = sum(len(files) for _, _, files in os.walk(source_dir))
            processed_files = 0
            
            def update_progress(current, total):
                progress_var.set((current / total) * 100)
                status_label.config(text=f"处理中: {current}/{total} 文件")
                progress.update()
                return not self.cancel_backup
            
            try:
                with ZipFile(backup_path, 'w', ZIP_DEFLATED, compresslevel=compression_level) as zipf:
                    for root, dirs, files in os.walk(source_dir):
                        # 排除文件夹
                        dirs[:] = [d for d in dirs if d not in self.config["exclude_folders"]]
                        
                        for file in files:
                            if self.cancel_backup:
                                raise Exception("备份已取消")
                                
                            # 排除指定扩展名的文件
                            if any(file.endswith(ext) for ext in self.config["exclude_extensions"]):
                                continue
                                
                            file_path = os.path.join(root, file)
                            arcname = os.path.relpath(file_path, start=source_dir)
                            zipf.write(file_path, arcname)
                            
                            processed_files += 1
                            if processed_files % 10 == 0:  # 每10个文件更新一次进度
                                update_progress(processed_files, total_files)
                
                # 清理旧备份
                self.cleanup_old_backups(backup_dir)
                
                progress.destroy()
                self.log(f"备份完成: {backup_path}")
                #messagebox.showinfo("成功", f"备份完成\n共备份 {processed_files} 个文件")
            except Exception as e:
                progress.destroy()
                if str(e) != "备份已取消":
                    self.log(f"备份失败: {str(e)}")
                    #messagebox.showerror("错误", f"备份失败: {str(e)}")
                else:
                    self.log("备份已取消")
                    if os.path.exists(backup_path):
                        os.remove(backup_path)
                    messagebox.showinfo("信息", "备份已取消")
        except Exception as e:
            self.log(f"备份失败: {str(e)}")
            #messagebox.showerror("错误", f"备份失败: {str(e)}")
    
    def cleanup_old_backups(self, backup_dir):
        max_backups = int(self.max_backups_spin.get())
        
        # 获取所有备份文件
        backups = []
        for file in os.listdir(backup_dir):
            if file.startswith("backup_") and file.endswith(".zip"):
                file_path = os.path.join(backup_dir, file)
                if os.path.isfile(file_path):
                    backups.append((os.path.getmtime(file_path), file_path))
        
        # 按修改时间排序
        backups.sort()
        
        # 删除多余的备份
        while len(backups) > max_backups:
            oldest = backups.pop(0)
            try:
                os.remove(oldest[1])
                self.log(f"删除旧备份: {os.path.basename(oldest[1])}")
            except Exception as e:
                self.log(f"删除旧备份失败: {os.path.basename(oldest[1])} - {str(e)}")
    
    def log(self, message):
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
        self.log_text.see(tk.END)
        self.root.update()
    
    def run_scheduler(self):
        while True:
            schedule.run_pending()
            tm.sleep(1)

if __name__ == "__main__":
    root = tk.Tk()
    app = BackupApp(root)
    root.mainloop()